package com.example.shop.user.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.shop.common.api.ServiceReturn;
import com.example.shop.common.api.ServiceStateCode;
import com.example.shop.common.util.GenerateCodeUtil;

import com.example.shop.mapper.*;
import com.example.shop.order.bo.Calculation;
import com.example.shop.order.bo.NormalCalculation;

import com.example.shop.order.dto.WXNativePayOrder;
import com.example.shop.order.dto.WXPayQueryResult;
import com.example.shop.order.enums.OrderState;
import com.example.shop.order.enums.PayStates;
import com.example.shop.order.service.WXNativePayService;
import com.example.shop.pojo.*;
import com.example.shop.rabbitmq.component.OrderComponents;
import com.example.shop.rabbitmq.dto.NoPayOrder;
import com.example.shop.redis.enums.RedisKey;
import com.example.shop.redis.service.LettuceClient;
import com.example.shop.user.vo.BuyResult;
import com.example.shop.user.dto.GoodsBuyInfo;
import com.example.shop.user.service.BuyGoodsService;
import com.example.shop.user.websocket.InformWebSocketServer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.uuid.Generators;
import com.rabbitmq.client.GetResponse;
import org.springframework.amqp.core.MessageDeliveryMode;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.websocket.EncodeException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;

import static org.springframework.transaction.annotation.Isolation.REPEATABLE_READ;

@Service
public class BuyGoodsServiceImpl implements BuyGoodsService {

    private final CourierMapper courierMapper;
    private final OrderMapper orderMapper;
    private final UserAddressBookMapper userAddressBookMapper;
    private final GoodsMapper goodsMapper;
    private final GoodsClassMapper goodsClassMapper;
    private final LettuceClient redisClient;
    private final WXNativePayService wxNativePayService;
    private final RabbitTemplate rabbitTemplate;

    @Qualifier("mqObjectMapper")
    @Resource
    ObjectMapper objectMapper;

    public BuyGoodsServiceImpl(CourierMapper courierMapper, OrderMapper orderMapper, UserAddressBookMapper userAddressBookMapper, GoodsMapper goodsMapper, GoodsClassMapper goodsClassMapper, LettuceClient redisClient, WXNativePayService wxNativePayService, RabbitTemplate rabbitTemplate) {
        this.courierMapper = courierMapper;
        this.orderMapper = orderMapper;
        this.userAddressBookMapper = userAddressBookMapper;
        this.goodsMapper = goodsMapper;
        this.goodsClassMapper = goodsClassMapper;
        this.redisClient = redisClient;
        this.wxNativePayService = wxNativePayService;
        this.rabbitTemplate = rabbitTemplate;
    }

    /**
     * 根据该联系人的地址生成物流单
     * @param deliveryAddress 收货地址
     * @return 返回物流单
     */
    private Courier generateCourier(UserAddressBook deliveryAddress){
        Courier courier = new Courier();
        courier.setCourierId(Generators.timeBasedGenerator().generate().node());
        courier.setRecipientName(deliveryAddress.getLinkmanName())
                .setRecipientPhone(deliveryAddress.getLinkmanPhone())
                .setRecipientArea(deliveryAddress.getLinkmanArea())
                .setRecipientDetailedAddress(deliveryAddress.getDetailedAddress())
                .setRecipientLongitude(deliveryAddress.getLinkmanLongitude())
                .setRecipientLatitude(deliveryAddress.getLinkmanLatitude());
        return courier;
    }

    /**
     * 生成订单
     * @param buyInfo 购买的信息
     * @param price 购买的商品款式的价格
     * @param courierId 物流单id
     * @return 返回生成的订单
     */
    private Order createOrder(long userId,GoodsBuyInfo buyInfo,int price,long courierId){
        Order order = new Order();
        //订单id
        order.setOrderId(Generators.timeBasedGenerator().generate().node())
                //该订单的归属用户的id
                .setUserId(userId)
                //该订单关联的商品id
                .setGoodsId(buyInfo.getGoodsId())
                //物流id
                .setCourierId(courierId)
                //购买的类型
                .setUserPurchasedGoodsClass(buyInfo.getBuyGoodsClass())
                //购买的数量
                .setUserPurchasedNumber(buyInfo.getBuyNum())
                //支付金额
                .setRealPay(calculationTotalAmount(new NormalCalculation(buyInfo,price)))
                //订单状态为待付款
                .setOrderState(OrderState.STAY_PAY.getOrderState())
                //支付方式
                .setPaymentMethod(buyInfo.getPayment().getPaymentMethod())
                //下单时间，即购买的时间
                .setOrderTime(LocalDateTime.now());
        return order;
    }

    /**
     * 根据计算的方式，计算付款总额
     * @param calculation 计算方式
     * @return 返回付款总额
     */
    private int calculationTotalAmount(Calculation calculation){
        return calculation.calculate();
    }

    //设置隔离级别为可重复读级别
    @Transactional(isolation = REPEATABLE_READ)
    @Override
    public ServiceReturn<BuyResult> buyGoods(User user, GoodsBuyInfo buyInfo,String webSocketSessionId) {
        long goodsId = buyInfo.getGoodsId();
        String buyGoodsClass = buyInfo.getBuyGoodsClass();
        //商品是否存在
        QueryWrapper<Goods> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("goods_id",goodsId);
        boolean existsGoods = goodsMapper.exists(queryWrapper);
        if(!existsGoods){
            return new ServiceReturn<>(ServiceStateCode.NOT_EXIST_GOODS,null);
        }
        //查看该联系人是否存在
        UserAddressBook addressBook = userAddressBookMapper.selectById(buyInfo.getLinkmanId());
        if(addressBook==null){
            return new ServiceReturn<>(ServiceStateCode.NOT_EXIST_LINKMAN,null);
        }

        //库存是否足够
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua\\inventoryChange.lua")));
        //lua并不支持Integer类型的返回值，所以使用long类型
        script.setResultType(Long.class);
        /**
         * lua脚本的执行，返回该款式的购买后剩余库存量。
         * 如果库存不足，则返回-3。
         * 如果款式不存在，则返回-2。
         * 如果访问不规范，即没有先访问该商品的详细数据，就直接进行购买，则返回-1
         * 如果购买的那一时刻，刚好赶上mq将redis的库存map同步到数据库和es中(从reds中读取出数据并写入到数据库和es中，最后删除掉键)，
         * 则会出现被买，但库存未减少的并发问题(解决方法：在redis中设置一个键值对来保证分布式数据的一致性)，
         * 这时应该阻止购买，则也是会返回-1
         */
        //redis中该商品对应的库存map
        String key1 = RedisKey.HashMap.GOODS_INVENTORY.getKey() + goodsId;
        //redis中是否正在进行redis的数据同步到mysql和es中。(如果有该键则代表不能更改redis中的对应的库存数据，即不能购买)
        String key2 = RedisKey.SYNC_ME_INVENTORY.getKey() + goodsId;
        Long surplus = redisClient.execLua(script,Arrays.asList(key1,key2), buyGoodsClass,buyInfo.getBuyNum());
        if (surplus == -1) {
            //访问不规范。没有先访问该商品的详细数据，就直接进行购买。不允许直接调用
            //购买异常，请重新购买，这时因为库存map写入到数据库和es的那段时间并发出问题，才会出现返回-4的情况
            return new ServiceReturn<>(ServiceStateCode.IRREGULAR_ACCESS, null);
        }else if(surplus == -2){
            //款式不存在
            return new ServiceReturn<>(ServiceStateCode.NOT_EXIST_GOODS_CLASS,null);
        }else if(surplus == -3) {
            //库存不足
            return new ServiceReturn<>(ServiceStateCode.INSUFFICIENT_INVENTORY, null);
        }


        //根据联系人地址生成物流单
        Courier courier = generateCourier(addressBook);
        //创建订单
        int price = goodsClassMapper.selectPrice(goodsId, buyGoodsClass);
        Order order = createOrder(user.getUserId(),buyInfo, price, courier.getCourierId());
        //将订单和物流信息保存到数据库
        courierMapper.insert(courier);
        orderMapper.insert(order);

        //设置微信支付二维码过期时间为30分钟
        Instant timeExpireInstant = LocalDateTime.now().toInstant(ZoneOffset.ofHoursMinutes(0, -30));
        LocalDateTime timeExpire = LocalDateTime.ofInstant(timeExpireInstant,ZoneOffset.ofHours(0));
        try {
            //用户支付成功时回调的url。如果将商品部署到阿里云服务器上这里要用公网的ip地址，而不是127.0.0.1
            String notifyUrl ="https://127.0.0.1:8080/user/goods/buy/success/"+order.getOrderId()+"/"+webSocketSessionId;
            //请求微信支付，返回微信支付请求的url
            String payUrl = wxNativePayService.createNativePayUrl(new WXNativePayOrder(order, buyGoodsClass, timeExpire, notifyUrl));
            //发布该订单到订单队列中，如果该订单超过30分钟没有付款，则该订单会被取消，且库存量会归还
            //如果支付成功，则会移除订单队列中该订单，并且该商品的被购买次数+1
            //消息发送前的消息处理
            MessagePostProcessor messagePostProcessor=(msg)->{
                MessageProperties messageProperties = msg.getMessageProperties();
                //消费者标签
                messageProperties.setConsumerTag(String.valueOf(user.getUserId()));
                //设置消息类型，默认application/octet-stream
                //设置为json字符串类型
                messageProperties.setContentType(MessageProperties.CONTENT_TYPE_JSON);
                //消息持久化
                messageProperties.setReceivedDeliveryMode(MessageDeliveryMode.PERSISTENT);
                return msg;
            };

            //消息的唯一id为订单id
            CorrelationData correlationData=new CorrelationData(String.valueOf(order.getOrderId()));
            //发布信息
            rabbitTemplate.convertAndSend(OrderComponents.ORDER_EXCHANGE,OrderComponents.NO_PAY_ROUTING_KEY,
                    new NoPayOrder(order,courier,payUrl), messagePostProcessor,correlationData);

            //将url生成二维码
            GenerateCodeUtil generateQRCode = GenerateCodeUtil.generateQRCode(payUrl, 300, 300);
            //生成购买的响应视图
            BuyResult buyResult = new BuyResult(generateQRCode.toStrBase64(),Math.toIntExact(surplus));
            return new ServiceReturn<>(ServiceStateCode.SUCCESS,buyResult);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new ServiceReturn<>(ServiceStateCode.FAILURE,null);
    }

    @Transactional(isolation = REPEATABLE_READ)
    @Override
    public ServiceReturn<Boolean> buySuccess(Long orderId, InformWebSocketServer socketServer) {
        Boolean ok = rabbitTemplate.execute(channel -> {
            Boolean result = false;
            Long preDeliveryTag = null;
            while (true) {
                //如果该消息队列中没有消息了，则应该不去拉取消息
                if (channel.messageCount(OrderComponents.NO_PAY_QUEUE) == 0) {
                    //如果之前还有消息没有被确认，则将所有未确认的消息进行不确认，返回队列中
                    if (preDeliveryTag != null) {
                        channel.basicNack(preDeliveryTag, true, true);
                    }
                    break;
                }
                //拉取消息
                GetResponse getResponse = channel.basicGet(OrderComponents.NO_PAY_QUEUE, false);
                //消息内容
                String json = new String(getResponse.getBody(), StandardCharsets.UTF_8);
                //将消息的内容转化为未支付订单对象
                NoPayOrder noPayOrder = objectMapper.readValue(json, NoPayOrder.class);
                //如果该消息是支付成功回调的该订单,则进入查看是否真的支付成功了
                if (noPayOrder.getOrder().getOrderId().equals(orderId)) {
                    WXPayQueryResult wxPayQueryResult = null;
                    try {
                        //查询一下微信那边是否真的支付了
                        wxPayQueryResult = wxNativePayService.queryOrder(orderId);
                    } catch (Exception e) {
                        e.printStackTrace();
                        break;
                    }
                    //如果查询结果是支付成功，则更新订单状态
                    if (wxPayQueryResult.getPayStates() == PayStates.SUCCESS) {
                        //修改订单状态为待发货、更新该订单的支付凭证,更新订单信息
                        Order order = wxPayQueryResult.toOrder();
                        order.setOrderState(OrderState.STAY_DELIVERY.getOrderState());
                        orderMapper.updateNotNull(order);
                        //更新商品购买次数
                        goodsMapper.updatePurchasedTime(noPayOrder.getOrder().getGoodsId(), noPayOrder.getOrder().getUserPurchasedNumber());
                        //并通知用户
                        try {
                            //如果存在websocket连接，则通知用户，订单支付成功
                            if (socketServer != null) {
                                socketServer.sendMessage("OK");
                            }
                        } catch (IOException e) {
                            e.printStackTrace();
                            break;
                        } catch (EncodeException e) {
                            e.printStackTrace();
                            break;
                        }
                        //只确认该消息，其他消息不确认全都重新入队
                        channel.basicAck(getResponse.getEnvelope().getDeliveryTag(), false);
                        result = true;
                    } else {
                        preDeliveryTag = getResponse.getEnvelope().getDeliveryTag();
                    }
                    //如果之前还有消息没有被确认，则将所有未确认的消息进行不确认，返回队列中
                    if (preDeliveryTag != null) {
                        channel.basicNack(preDeliveryTag, true, true);
                    }
                    break;
                }
                //等待被确认
                channel.waitForConfirms();
                //记录上一个消息
                preDeliveryTag = getResponse.getEnvelope().getDeliveryTag();
            }
            return result;
        });
        return new ServiceReturn<>(ServiceStateCode.SUCCESS,ok);
    }
}
